package studio.baka.satoripixeldungeon.items;

import studio.baka.satoripixeldungeon.Assets;
import studio.baka.satoripixeldungeon.Badges;
import studio.baka.satoripixeldungeon.Dungeon;
import studio.baka.satoripixeldungeon.SatoriPixelDungeon;
import studio.baka.satoripixeldungeon.actors.Actor;
import studio.baka.satoripixeldungeon.actors.Char;
import studio.baka.satoripixeldungeon.actors.hero.Hero;
import studio.baka.satoripixeldungeon.effects.Speck;
import studio.baka.satoripixeldungeon.items.bags.Bag;
import studio.baka.satoripixeldungeon.journal.Catalog;
import studio.baka.satoripixeldungeon.mechanics.Ballistica;
import studio.baka.satoripixeldungeon.messages.Messages;
import studio.baka.satoripixeldungeon.scenes.CellSelector;
import studio.baka.satoripixeldungeon.scenes.GameScene;
import studio.baka.satoripixeldungeon.sprites.ItemSprite;
import studio.baka.satoripixeldungeon.sprites.MissileSprite;
import studio.baka.satoripixeldungeon.ui.QuickSlotButton;
import studio.baka.satoripixeldungeon.utils.GLog;
import com.watabou.noosa.audio.Sample;
import com.watabou.noosa.particles.Emitter;
import com.watabou.utils.Bundlable;
import com.watabou.utils.Bundle;
import com.watabou.utils.Reflection;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Objects;

public class Item implements Bundlable {

    protected static final String TXT_TO_STRING_LVL = "%s %+d";
    protected static final String TXT_TO_STRING_X = "%s x%d";

    protected static final float TIME_TO_THROW = 1.0f;
    protected static final float TIME_TO_PICK_UP = 1.0f;
    protected static final float TIME_TO_DROP = 1.0f;

    public static final String AC_DROP = "DROP";
    public static final String AC_THROW = "THROW";

    public String defaultAction;
    public boolean usesTargeting;

    protected String name = Messages.get(this, "name");
    public int image = 0;

    public boolean stackable = false;
    protected int quantity = 1;
    public boolean dropsDownHeap = false;

    private int level = 0;

    public boolean levelKnown = false;

    public boolean cursed;
    public boolean cursedKnown;

    // Unique items persist through revival
    public boolean unique = false;

    // whether an item can be included in heroes remains
    public boolean bones = false;

    private static final Comparator<Item> itemComparator = Comparator.comparingInt(Generator.Category::order);

    public static Item virtual(Class<? extends Item> aClass) {
        try {
            Item item = aClass.newInstance();
            item.quantity = 0;
            return item;
        } catch (Exception e) {
            SatoriPixelDungeon.reportException(e);
            return null;
        }
    }

    public ArrayList<String> actions(Hero hero) {
        ArrayList<String> actions = new ArrayList<>();
        actions.add(AC_DROP);
        actions.add(AC_THROW);
        return actions;
    }

    public boolean doPickUp(Hero hero) {
        if (collect(hero.belongings.backpack)) {

            GameScene.pickUp(this, hero.pos);
            Sample.INSTANCE.play(Assets.SND_ITEM);
            hero.spendAndNext(TIME_TO_PICK_UP);
            return true;

        } else {
            return false;
        }
    }

    public void doDrop(Hero hero) {
        hero.spendAndNext(TIME_TO_DROP);
        Dungeon.level.drop(detachAll(hero.belongings.backpack), hero.pos).sprite.drop(hero.pos);
    }

    //resets an item's properties, to ensure consistency between runs
    public void reset() {
        //resets the name incase the language has changed.
        name = Messages.get(this, "name");
    }

    public void doThrow(Hero hero) {
        GameScene.selectCell(thrower);
    }

    public void execute(Hero hero, String action) {

        curUser = hero;
        curItem = this;

        if (action.equals(AC_DROP)) {

            if (hero.belongings.backpack.contains(this) || isEquipped(hero)) {
                doDrop(hero);
            }

        } else if (action.equals(AC_THROW)) {

            if (hero.belongings.backpack.contains(this) || isEquipped(hero)) {
                doThrow(hero);
            }

        }
    }

    public void execute(Hero hero) {
        execute(hero, defaultAction);
    }

    protected void onThrow(int cell) {
        Heap heap = Dungeon.level.drop(this, cell);
        if (!heap.isEmpty()) {
            heap.sprite.drop(cell);
        }
    }

    //takes two items and merges them (if possible)
    public Item merge(Item other) {
        if (isSimilar(other)) {
            quantity += other.quantity;
            other.quantity = 0;
        }
        return this;
    }

    public boolean collect(Bag container) {

        ArrayList<Item> items = container.items;

        if (items.contains(this)) {
            return true;
        }

        for (Item item : items) {
            if (item instanceof Bag && ((Bag) item).grab(this)) {
                return collect((Bag) item);
            }
        }

        if (stackable) {
            for (Item item : items) {
                if (isSimilar(item)) {
                    item.merge(this);
                    updateQuickslot();
                    return true;
                }
            }
        }

        if (items.size() < container.size) {

            if (Dungeon.hero != null && Dungeon.hero.isAlive()) {
                Badges.validateItemLevelAquired(this);
            }

            items.add(this);
            Dungeon.quickslot.replacePlaceholder(this);
            updateQuickslot();
            items.sort(itemComparator);
            return true;

        } else {

            GLog.n(Messages.get(Item.class, "pack_full", name()));
            return false;

        }
    }

    public boolean collect() {
        return collect(Dungeon.hero.belongings.backpack);
    }

    //returns a new item if the split was sucessful and there are now 2 items, otherwise null
    public Item split(int amount) {
        if (amount <= 0 || amount >= quantity()) {
            return null;
        } else {
            //pssh, who needs copy constructors?
            Item split = Reflection.newInstance(getClass());

            if (split == null) {
                return null;
            }

            Bundle copy = new Bundle();
            this.storeInBundle(copy);
            split.restoreFromBundle(copy);
            split.quantity(amount);
            quantity -= amount;

            return split;
        }
    }

    public final Item detach(Bag container) {

        if (quantity <= 0) {

            return null;

        } else if (quantity == 1) {

            if (stackable) {
                Dungeon.quickslot.convertToPlaceholder(this);
            }

            return detachAll(container);

        } else {


            Item detached = split(1);
            updateQuickslot();
            if (detached != null) detached.onDetach();
            return detached;

        }
    }

    public final Item detachAll(Bag container) {
        Dungeon.quickslot.clearItem(this);
        updateQuickslot();

        for (Item item : container.items) {
            if (item == this) {
                container.items.remove(this);
                item.onDetach();
                return this;
            } else if (item instanceof Bag) {
                Bag bag = (Bag) item;
                if (bag.contains(this)) {
                    return detachAll(bag);
                }
            }
        }

        return this;
    }

    public boolean isSimilar(Item item) {
        return level == item.level && getClass() == item.getClass();
    }

    protected void onDetach() {
    }

    public int level() {
        return level;
    }

    public void level(int value) {
        level = value;

        updateQuickslot();
    }

    public Item upgrade() {

        this.level++;

        updateQuickslot();

        return this;
    }

    final public Item upgrade(int n) {
        for (int i = 0; i < n; i++) {
            upgrade();
        }

        return this;
    }

    public Item degrade() {

        this.level--;

        return this;
    }

    final public Item degrade(int n) {
        for (int i = 0; i < n; i++) {
            degrade();
        }

        return this;
    }

    public int visiblyUpgraded() {
        return levelKnown ? level() : 0;
    }

    public boolean visiblyCursed() {
        return cursed && cursedKnown;
    }

    public boolean isUpgradable() {
        return true;
    }

    public boolean isIdentified() {
        return levelKnown && cursedKnown;
    }

    public boolean isEquipped(Hero hero) {
        return false;
    }

    public Item identify() {

        levelKnown = true;
        cursedKnown = true;

        if (Dungeon.hero != null && Dungeon.hero.isAlive()) {
            Catalog.setSeen(getClass());
        }

        return this;
    }

    public void onHeroGainExp(float levelPercent, Hero hero) {
        //do nothing by default
    }

    public static void evoke(Hero hero) {
        hero.sprite.emitter().burst(Speck.factory(Speck.EVOKE), 5);
    }

    @Override
    public String toString() {

        String name = name();

        if (visiblyUpgraded() != 0)
            name = Messages.format(TXT_TO_STRING_LVL, name, visiblyUpgraded());

        if (quantity > 1)
            name = Messages.format(TXT_TO_STRING_X, name, quantity);

        return name;

    }

    public String name() {
        return name;
    }

    public final String trueName() {
        return name;
    }

    public int image() {
        return image;
    }

    public ItemSprite.Glowing glowing() {
        return null;
    }

    public Emitter emitter() {
        return null;
    }

    public String info() {
        return desc();
    }

    public String desc() {
        return Messages.get(this, "desc");
    }

    public int quantity() {
        return quantity;
    }

    public Item quantity(int value) {
        quantity = value;
        return this;
    }

    public int price() {
        return 0;
    }

    public Item virtual() {
        Item item = Reflection.newInstance(getClass());
        if (item == null) return null;

        item.quantity = 0;
        item.level = level;
        return item;
    }

    public static boolean targetAction(String action) {
        return action.equals("THROW") || action.equals("ZAP") || action.equals("LIGHTTHROW") || action.equals("CAST") || action.equals("SHOOT");
    }

    public Item random() {
        return this;
    }

    public String status() {
        return quantity != 1 ? Integer.toString(quantity) : null;
    }

    public static void updateQuickslot() {
        QuickSlotButton.refresh();
    }

    private static final String QUANTITY = "quantity";
    private static final String LEVEL = "level";
    private static final String LEVEL_KNOWN = "levelKnown";
    private static final String CURSED = "cursed";
    private static final String CURSED_KNOWN = "cursedKnown";
    private static final String QUICKSLOTSET = "quickslotposset";
    private static final String QUICKACTIONLIST = "quickactionlist";

    @Override
    public void storeInBundle(Bundle bundle) {
        bundle.put(QUANTITY, quantity);
        bundle.put(LEVEL, level);
        bundle.put(LEVEL_KNOWN, levelKnown);
        bundle.put(CURSED, cursed);
        bundle.put(CURSED_KNOWN, cursedKnown);
        if (Dungeon.quickslot.contains(this)) {
            bundle.put(QUICKSLOTSET, Dungeon.quickslot.getSlotSet(this));
            bundle.put(QUICKACTIONLIST, Dungeon.quickslot.getAction(this));
        }
    }

    @Override
    public void restoreFromBundle(Bundle bundle) {
        quantity = bundle.getInt(QUANTITY);
        levelKnown = bundle.getBoolean(LEVEL_KNOWN);
        cursedKnown = bundle.getBoolean(CURSED_KNOWN);

        int level = bundle.getInt(LEVEL);
        if (level > 0) {
            upgrade(level);
        } else if (level < 0) {
            degrade(-level);
        }

        cursed = bundle.getBoolean(CURSED);

        //only want to populate slot on first load.
        if (Dungeon.hero == null) {
			/*
			if (bundle.contains(QUICKSLOT)) {
				Dungeon.quickslot.setSlot(bundle.getInt(QUICKSLOT), this);
			}
			*/
            if (bundle.contains(QUICKSLOTSET) && bundle.contains(QUICKACTIONLIST)) {
                //Dungeon.quickslot.setSlot(bundle.getInt(QUICKSLOT), this, bundle.getString(QUICKACTION));
                ArrayList<Integer> j = new ArrayList<>();
                boolean[] k = bundle.getBooleanArray(QUICKSLOTSET);
                for (int i = 0; i < k.length; i++) {
                    if (k[i]) j.add(i);
                }

                int l = 0;
                for (String i : bundle.getStringArray(QUICKACTIONLIST)) {
                    Dungeon.quickslot.setSlot(j.get(l), this, i);
                    l++;
                }


            }
        }
    }

    public int throwPos(Hero user, int dst) {
        return new Ballistica(user.pos, dst, Ballistica.PROJECTILE).collisionPos;
    }

    public void cast(final Hero user, final int dst) {

        final int cell = throwPos(user, dst);
        user.sprite.zap(cell);
        user.busy();

        Sample.INSTANCE.play(Assets.SND_MISS, 0.6f, 0.6f, 1.5f);

        Char enemy = Actor.findChar(cell);
        QuickSlotButton.target(enemy);

        final float delay = castDelay(user, dst);

        if (enemy != null) {
            ((MissileSprite) user.sprite.parent.recycle(MissileSprite.class)).
                    reset(user.sprite,
                            enemy.sprite,
                            this,
                            () -> {
                                curUser = user;
                                Objects.requireNonNull(Item.this.detach(user.belongings.backpack)).onThrow(cell);
                                user.spendAndNext(delay);
                            });
        } else {
            ((MissileSprite) user.sprite.parent.recycle(MissileSprite.class)).
                    reset(user.sprite,
                            cell,
                            this,
                            () -> {
                                curUser = user;
                                Objects.requireNonNull(Item.this.detach(user.belongings.backpack)).onThrow(cell);
                                user.spendAndNext(delay);
                            });
        }
    }

    public float castDelay(Char user, int dst) {
        return TIME_TO_THROW;
    }

    protected static Hero curUser = null;
    protected static Item curItem = null;
    protected static CellSelector.Listener thrower = new CellSelector.Listener() {
        @Override
        public void onSelect(Integer target) {
            if (target != null) {
                curItem.cast(curUser, target);
            }
        }

        @Override
        public String prompt() {
            return Messages.get(Item.class, "prompt");
        }
    };
}
